Skip to content

S01-06 基础-数组

[TOC]

概述

为什么需要数组

一个养鸡场有 6 只鸡,它们的体重分别是 3kg、5kg、1kg、3.4kg、2kg、50kg 。请问这六只鸡的总体重是多少?平均体重是多少?请你编一个程序(Array01.java)

思路:定义 6 个变量,加起来求总体重,进而求出平均体重。引出-> 数组

数组介绍

数组:是一种引用数据类型,用于存储固定长度相同数据类型的元素集合。它将多个同类型的变量按连续的内存地址组织起来,通过 下标(索引) 快速访问每个元素,是处理批量同类型数据的基础结构。

即:数(数据)组(一组)就是一组数据

快速入门:养鸡场

比如,我们可以用数组来解决上一个问题,体验数组的使用:

java
public class Array01 {
    //编写一个main方法
    public static void main(String[] args) {
        /*
        它们的体重分别是3kg,5kg,1kg,3.4kg,2kg,50kg 。
        请问这六只鸡的总体重是多少?平均体重是多少?
        思路分析
        1. 定义六个变量double , 求和得到总体重
        2. 平均体重= 总体重/ 6
        3. 分析传统实现的方式问题:6->600->566
        4. 引出新的技术-> 使用数组来解决.
        */
        // 传统方式实现(注释掉,用于对比)
        // double hen1 = 3;
        // double hen2 = 5;
        // double hen3 = 1;
        // double hen4 = 3.4;
        // double hen5 = 2;
        // double hen6 = 50;
        // double totalWeight = hen1 + hen2 + hen3 + hen4 + hen5 + hen6;
        // double avgWeight = totalWeight / 6;
        // System.out.println("总体重=" + totalWeight + " 平均体重=" + avgWeight);

        // 用数组解决
        // 老韩解读
        // 1. double[] 表示是double类型的数组,数组名hens
        // 2. {3, 5, 1, 3.4, 2, 50, 7.8, 88.8, 1.1, 5.6, 100} 表示数组的值/元素,依次对应数组的各个元素
        double[] hens = {3, 5, 1, 3.4, 2, 50, 7.8, 88.8, 1.1, 5.6, 100};

        System.out.println("===使用数组解决===");
        // 老师提示:可以通过数组名.length 得到数组的大小/长度
        // System.out.println("数组的长度=" + hens.length);

        double totalWeight = 0;
        // 遍历数组得到所有元素的和
        // 老韩解读
        // 1. 可以通过hens[下标] 访问数组的元素,下标从0开始(第一个元素hens[0],第二个hens[1],依次类推)
        // 2. 通过for循环访问数组的所有元素
        // 3. 使用变量totalWeight累积各个元素的值
        for (int i = 0; i < hens.length; i++) {
            // System.out.println("第" + (i+1) + "个元素的值=" + hens[i]);
            totalWeight += hens[i];
        }

        System.out.println("总体重=" + totalWeight + " 平均体重=" + (totalWeight / hens.length));
    }
}

数组基础

数组初始化

方式 1:动态初始化-声明时赋值

语法

java
// 语法
数据类型 数组名[] = new 数据类型[大小]

// 示例:
int a[] = new int[5]; // 创建了一个int类型数组,名字a,可存放5个int值

说明:这是定义数组的一种方法,结合数组内存图理解

image-20251216164223913

示例:快速入门

循环输入 5 个成绩,保存到 double 数组,并输出:

java
import java.util.Scanner;

public class Array02 {
    public static void main(String[] args) {
        // 演示动态初始化数组
        // 循环输入5个成绩,保存到double数组,并输出

        // 步骤
        // 1. 创建double数组,大小5
        // (1) 第一种动态分配方式:double scores[] = new double[5];
        // (2) 第二种动态分配方式:先声明数组,再分配内存空间
        double scores[]; // 声明数组,此时scores是null
        scores = new double[5]; // 分配内存空间,可存放5个double数据

        // 2. 循环输入成绩
        Scanner myScanner = new Scanner(System.in);
        for (int i = 0; i < scores.length; i++) {
            System.out.println("请输入第" + (i+1) + "个成绩:");
            scores[i] = myScanner.nextDouble();
        }

        // 3. 输出数组元素
        System.out.println("==数组的元素/值的情况如下:===");
        for (int i = 0; i < scores.length; i++) {
            System.out.println("第" + (i+1) + "个元素的值=" + scores[i]);
        }
    }
}

方式 2:动态初始化-先声明再赋值

  1. 声明数组

    java
    // 语法
    数据类型 数组名[];
    数据类型[] 数组名;
    
    // 示例
    int a[];
    int[] a;
  2. 创建数组(分配内存)

    java
    // 语法
    数组名 = new 数据类型[大小];
    
    // 示例
    a = new int[10];

案例演示:基于前面的代码修改即可

方式 3:静态初始化

语法

如果已知数组的元素个数和具体值,可直接使用静态初始化。

等价于:先创建指定大小的数组,再逐个给元素赋值。

java
数据类型 数组名[] = {元素值1, 元素值2, ..., 元素值n};

示例

java
// 静态初始化
int a[] = {2,5,6,7,8,89,90,34,56};

// 等价于动态初始化的如下写法
int a[] = new int[9];
a[0] = 2; a[1] = 5; a[2] = 6; a[3] = 7; a[4] = 8;
a[5] = 89; a[6] = 90; a[7] = 34; a[8] = 56;

// 养鸡场案例中的数组(静态初始化)
double hens[] = {3, 5, 1, 3.4, 2, 50};
// 等价于
double hens[] = new double[6];
hens[0] = 3; hens[1] = 5; hens[2] = 1; hens[3] = 3.4; hens[4] = 2; hens[5] = 50;

数组的访问

语法

java
// 语法
数组名[下标/索引/index]

// 示例:
a[2] // 访问a数组的第3个元素,数组下标从0开始

数组核心特性

  1. 数组是多个相同类型数据的组合,实现对这些数据的统一管理。

  2. 数组中的元素可以是任何数据类型(包括基本类型和引用类型),但不能混用

  3. 数组创建后如果没有赋值,会有默认值

    • 基本类型:
      • int 0short 0byte 0long 0
      • float 0.0double 0.0
      • char \u0000
      • boolean false
    • 引用类型(如 String):null
  4. 使用数组的步骤:

    1. 声明数组并开辟空间
    2. 给数组元素赋值
    3. 使用数组
  5. 数组的下标从0开始。

  6. 数组下标必须在指定范围内使用(有效范围:0 ~ length-1),否则报下标越界异常

    java
    // 有效下标为0-4,访问 arr[5] 会报错
    int[] arr = new int[5];
  7. 数组属于引用类型,数组型数据是对象(object)。

示例:验证代码

java
public class ArrayDetail {
    //编写一个main方法
    public static void main(String[] args) {
        // 1. 数组元素类型必须相同(错误示例:int数组中不能放String)
        // int[] arr1 = {1, 2, 3, 60, "hello"}; // 编译错误:String无法转换为int
        double[] arr2 = {1.1, 2.2, 3.3, 60.6, 100}; // 正确:int自动转换为double

        // 2. 数组元素可以是引用类型(示例:String数组)
        String[] arr3 = {"北京", "jack", "milan"};

        // 3. 数组未赋值时的默认值
        short[] arr4 = new short[3];
        System.out.println("=====数组arr4(未赋值的默认值)=====");
        for (int i = 0; i < arr4.length; i++) {
            System.out.println(arr4[i]); // 输出3个0(short类型默认值)
        }

        // 6. 数组下标越界演示(注释掉,避免运行报错)
        // int[] arr = new int[5];
        // System.out.println(arr[5]); // 运行时异常:ArrayIndexOutOfBoundsException
    }
}

数组应用

应用 1:创建 26 个字母数组

创建一个 char 类型的 26 个元素的数组,分别放置'A'-'Z',使用 for 循环访问所有元素并打印。

提示:char 类型支持运算('A' + 2 → 'C'

java
public class ArrayExercise01 {
    public static void main(String[] args) {
        /*
        思路分析
        1. 定义char数组,大小26
        2. 利用'A' + i 的运算赋值(i从0到25)
        3. 循环访问并打印所有元素
        */
        char[] chars = new char[26];
        // 赋值:A~Z
        for (int i = 0; i < chars.length; i++) {
            // 'A' + i 是int类型,需要强制转换为char
            chars[i] = (char) ('A' + i);
        }
        // 输出
        System.out.println("===chars数组===");
        for (int i = 0; i < chars.length; i++) {
            System.out.print(chars[i] + " "); // 输出:A B C ... Z
        }
    }
}

应用 2:求数组最大值及下标

请求出数组int[] arr = {4, -1, 9, 10, 23}的最大值,并得到对应的下标。

java
public class ArrayExercise02 {
    //编写一个main方法
    public static void main(String[] args) {
        /*
        老韩思路分析
        1. 定义目标数组
        2. 假定第一个元素为最大值(max = arr[0]),下标为maxIndex=0
        3. 从下标1开始遍历数组,若当前元素>max,则更新max和maxIndex
        4. 遍历结束后,max即为最大值,maxIndex为对应下标
        */
        int[] arr = {4, -1, 9, 10, 23};
        int max = arr[0]; // 假定第一个元素是最大值
        int maxIndex = 0; // 最大值的初始下标

        // 从下标1开始遍历
        for (int i = 1; i < arr.length; i++) {
            if (max < arr[i]) { // 若当前元素大于max
                max = arr[i]; // 更新最大值
                maxIndex = i; // 更新最大值下标
            }
        }

        System.out.println("max=" + max + " maxIndex=" + maxIndex); // 输出:max=23 maxIndex=4
    }
}

应用 3:求数组的和和平均值

基于养鸡场案例,直接通过数组遍历求和,再计算平均值(已在 Array01.java 中实现)。

对象数组

什么是对象数组

在 Java 中,对象数组(Object Array) 是一个非常基础但也极其容易让初学者踩坑的概念。

简单来说:如果说普通数组(如 int[])是一个装满数字的盒子,那么对象数组就是一个装满“遥控器”的盒子,每个遥控器分别指向一台真正的“电视机”(对象)。

本质它存的是内存地址的引用

在 Java 中,基本数据类型(如 int, double)和引用数据类型(如 String, 自定义类)在内存中的存储方式完全不同。

  • 基本类型数组(如 int[]:数组的每个格子里,直接存着具体的数值(比如 10, 20)。
  • 对象类型数组(如 User[]:数组的每个格子里,存的仅仅是内存地址(引用),而不是真正的对象。

创建对象数组

创建对象数组的“标准三步曲”:

这是新手最容易犯错的地方。创建一个对象数组,必须经历完整的三个步骤,缺一不可:

  1. 步骤 1:定义一个类(设计图纸):

    假设我们有一个简单的 Student 类:

    java
    class Student {
        String name;
        int age;
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
  2. 步骤 2:创建数组(造盒子):

    java
    // 声明并创建一个长度为 3 的 Student 数组
    Student[] students = new Student[3];

    ** 极其重要**:执行完这句代码后,内存中只是有了 3 个位置,但这 3 个位置里全部都是 null。这时候如果你直接去调用 students[0].name,程序会立刻抛出 NullPointerException(空指针异常)!

  3. 步骤 3:实例化对象并放入数组(买电视并配对遥控器):

    你必须挨个(或通过循环)把真正的对象 new 出来,放进数组的格子里:

    java
    students[0] = new Student("张三", 18);
    students[1] = new Student("李四", 20);
    students[2] = new Student("王五", 19);

    或者,你可以使用静态初始化一步到位,代码更简洁:

    java
    Student[] students = {
        new Student("张三", 18),
        new Student("李四", 20),
        new Student("王五", 19)
    };

image-20260302145131819

遍历对象数组

遍历对象数组与普通数组没有区别,强烈推荐使用你之前了解过的 Foreach 增强型循环

java
for (Student stu : students) {
    // 每次循环,stu 就会指向数组中的下一个真实对象
    System.out.println("姓名:" + stu.name + ",年龄:" + stu.age);
}

致命缺陷

对象数组的致命缺陷:

虽然对象数组是 Java 底层不可或缺的基础,但在现代的实际业务开发中,我们极少直接使用对象数组,原因有两点:

  1. 长度固定死板:一旦你声明了 new Student[3],这个数组就永远只能装 3 个人。如果中途转学来了一个新同学,你无法把他直接塞进去,只能重新建一个更大的数组,再把老数据搬过去,非常麻烦。

  2. 功能匮乏:数组没有自带好用的方法,比如“删除某个元素”、“判断是否包含某个对象”等,都需要你自己手写循环去实现。

现代替代方案

因为上述缺陷,日常开发中我们几乎总是使用 集合(如 ArrayList<Student> 来替代对象数组。ArrayList 底层其实也就是一个对象数组,只不过 Java 帮我们封装好了自动扩容、增删改查的所有逻辑。

数组操作

数组赋值机制

基本类型赋值

赋值的是具体的数据,两个变量相互独立,修改一个不影响另一个。

java
int n1 = 2;
int n2 = n1;
n2 = 80;
System.out.println("n1=" + n1); // 输出10(不受n2修改影响)
System.out.println("n2=" + n2); // 输出80

引用赋值

数组默认是引用传递,赋值的是数组的地址,两个数组变量指向同一个内存空间。

修改其中一个数组的元素,会影响另一个数组。


示例

java
public class ArrayAssign {
  public static void main(String[] args) {
    int[] arr1 = {1, 2, 3};
    int[] arr2 = arr1; // 引用传递:arr2指向arr1的内存地址

    arr2[0] = 10; // 修改arr2的元素

    // 输出arr1和arr2的元素
    System.out.println("====arr1的元素====");
    for (int i = 0; i < arr1.length; i++) {
      System.out.println(arr1[i]); // 输出:10, 2, 3(arr1的元素被修改)
    }
    System.out.println("====arr2的元素====");
    for (int i = 0; i < arr2.length; i++) {
      System.out.println(arr2[i]); // 输出:10, 2, 3
    }
  }
}

内存解析

  • 栈内存:存储变量(arr1、arr2),保存数组的内存地址(如 0x0011)。
  • 堆内存:存储数组的实际元素(1,2,3),地址为 0x0011。
  • arr1 和 arr2 都指向 0x0011,修改 arr2[0]本质是修改堆内存中数组的元素。

image-20251216164932336

数组拷贝

要求:实现数组内容的复制,新数组与原数组的数据空间独立(修改新数组不影响原数组)。

java
public class ArrayCopy {
  //编写一个main方法
  public static void main(String[] args) {
    // 原数组
    int[] arr1 = {10, 20, 30};

    // 1. 创建新数组,开辟独立的内存空间(大小与原数组一致)
    int[] arr2 = new int[arr1.length];

    // 2. 遍历原数组,将元素逐个拷贝到新数组
    for (int i = 0; i < arr1.length; i++) {
      arr2[i] = arr1[i];
    }

    // 修改新数组的元素(验证独立性)
    arr2[0] = 100;

    // 输出原数组(不受影响)
    System.out.println("====arr1的元素====");
    for (int i = 0; i < arr1.length; i++) {
      System.out.println(arr1[i]); // 输出:10, 20, 30
    }

    // 输出新数组(已修改)
    System.out.println("====arr2的元素====");
    for (int i = 0; i < arr2.length; i++) {
      System.out.println(arr2[i]); // 输出:100, 20, 30
    }
  }
}

数组反转

要求:将数组元素反转(如{11,22,33,44,55,66}{66,55,44,33,22,11}

方式 1:原地反转

思路分析

  1. 交换规律:arr[i]arr[arr.length - 1 - i] 交换(i 从 0 开始)。
  2. 交换次数:arr.length / 2(如 6 个元素交换 3 次,5 个元素交换 2 次)。

代码实现

java
public class ArrayReverse {
  public static void main(String[] args) {
    int[] arr = {11, 22, 33, 44, 55, 66};
    int temp = 0; // 辅助交换的临时变量
    int len = arr.length; // 数组长度(优化:避免重复计算)

    // 交换逻辑
    for (int i = 0; i < len / 2; i++) {
      temp = arr[len - 1 - i]; // 保存末尾元素
      arr[len - 1 - i] = arr[i]; // 前面元素移到末尾
      arr[i] = temp; // 末尾元素移到前面
    }

    // 输出反转后的数组
    System.out.println("===翻转后数组===");
    for (int i = 0; i < arr.length; i++) {
      System.out.print(arr[i] + "\t"); // 输出:66	55	44	33	22	11
    }
  }
}

方式 2:逆序赋值

思路分析

  1. 创建与原数组大小相同的新数组arr2
  2. 逆序遍历原数组,将元素依次赋值给新数组的正序位置。
  3. 让原数组变量arr指向新数组(原数组内存空间变为垃圾,被 JVM 回收)。

image-20251216165041752

代码实现

java
public class ArrayReverse02 {
  public static void main(String[] args) {
    int[] arr = {11, 22, 33, 44, 55, 66};
    // 创建新数组
    int[] arr2 = new int[arr.length];

    // 逆序遍历原数组,赋值给新数组
    for (int i = arr.length - 1, j = 0; i >= 0; i--, j++) {
      arr2[j] = arr[i];
    }

    // 原数组变量指向新数组
    arr = arr2;

    // 输出结果
    System.out.println("====arr的元素情况=====");
    for (int i = 0; i < arr.length; i++) {
      System.out.print(arr[i] + "\t"); // 输出:66	55	44	33	22	11
    }
  }
}

数组扩容@

要求:实现动态给数组添加元素,支持用户自主选择是否继续添加(ArrayAdd02.java)

需求

  1. 原始数组:int[] arr = {1,2,3}(静态分配)。
  2. 每次添加元素到数组末尾(如添加 4 → {1,2,3,4})。
  3. 提示用户输入添加的元素,添加成功后询问是否继续(y/n)。

思路分析

  1. 每次添加时,创建新数组(大小=原数组长度+1)。
  2. 将原数组元素拷贝到新数组。
  3. 新元素赋值给新数组的最后一个位置。
  4. 原数组变量指向新数组(完成扩容)。
  5. 使用do-while循环+break控制用户输入逻辑。

image-20260226115601132

代码实现

java
import java.util.Scanner;

public class ArrayAdd02 {
  public static void main(String[] args) {
    Scanner myScanner = new Scanner(System.in);
    // 初始化原数组
    int[] arr = {1, 2, 3};

    // 1. 创建新数组(扩容为5)
    int[] arrNew = new int[5];

    // 2. 拷贝原数组元素到新数组
    for (int i = 0; i < arr.length; i++) {
      arrNew[i] = arr[i];
    }

    // 3. 原数组指向新数组
    arr = arrNew;

    // 4. 新元素放入新数组末尾(可选)
    arr[3] = 4;
    arr[4] = 5;

    // 5. 输出扩容后的数组
    System.out.println("====arr扩容后元素情况====");
    for (int i = 0; i < arr.length; i++) {
      System.out.print(arr[i] + "\t");
    }
  }
}

数组合并

需求:合并数组 {1,2,3}{4,5,6}

image-20260226154211638

代码实现

image-20260226154416980

练习:数组缩减

需求:有一个数组{1,2,3,4,5},提示用户是否继续缩减,每次缩减最后一个元素。当只剩最后一个元素时,提示“不能再缩减”。

image-20251216165144848

二维数组

多维数组中最常用的是二维数组,应用场景如:五子棋棋盘(行列坐标)、表格数据等。

快速入门

需求:用二维数组输出如下图形:

0 0 0 0 0 0
0 0 1 0 0 0
0 2 0 3 0 0
0 0 0 0 0 0

代码实现

java
public class TwoDimensionalArray01 {
    public static void main(String[] args) {
        /*
        二维数组定义:
        1. 从形式上看:int[][] 表示二维数组(一维数组的每个元素是一维数组)
        2. 初始化:{ {0,0,0,0,0,0}, {0,0,1,0,0,0}, {0,2,0,3,0,0}, {0,0,0,0,0,0} }
        */
        int[][] arr = {
                {0, 0, 0, 0, 0, 0},
                {0, 0, 1, 0, 0, 0},
                {0, 2, 0, 3, 0, 0},
                {0, 0, 0, 0, 0, 0}
        };

        // 二维数组的关键概念
        System.out.println("二维数组的元素个数(一维数组的个数)=" + arr.length); // 输出4(4个一维数组)
        // 访问指定元素:第3个一维数组的第4个值(arr[2][3],下标从0开始)
        System.out.println("第3个一维数组的第4个值=" + arr[2][3]); // 输出3

        // 遍历二维数组(嵌套循环)
        System.out.println("===二维数组图形输出===");
        for (int i = 0; i < arr.length; i++) { // 遍历二维数组的每个一维数组
            for (int j = 0; j < arr[i].length; j++) { // 遍历当前一维数组的每个元素
                System.out.print(arr[i][j] + " ");
            }
            System.out.println(); // 换行
        }
    }
}

初始化

方式 1:动态初始化-固定行列

语法

java
类型[][] 数组名 = new 类型[行数][列数]

示例

java
public class TwoDimensionalArray02 {
  public static void main(String[] args) {
    // 方式1:直接初始化
    // int[][] arr = new int[2][3]; // 2行3列的二维数组

    // 方式2:先声明,再分配空间
    int[][] arr;
    arr = new int[2][3]; // 2个一维数组,每个一维数组有3个元素
    arr[1][1] = 8; // 给第2行第2列的元素赋值8

    // 遍历二维数组
    System.out.println("===二维数组输出===");
    for (int i = 0; i < arr.length; i++) {
      for (int j = 0; j < arr[i].length; j++) {
        System.out.print(arr[i][j] + " ");
      }
      System.out.println();
    }
  }
}

内存解析

  • 栈内存arr 存储二维数组的地址(如 0x0011)。
  • 堆内存
    • 二维数组(地址 0x0011)存储两个一维数组的地址(如 0x0022、0x0033)。
    • 一维数组(0x0022):[0,0,0](默认值)。
    • 一维数组(0x0033):[0,8,0](arr[1][1]赋值为 8)。

image-20251216165917783

方式 3:动态初始化-列数不确定

需求:动态创建如下二维数组(每行列数不同),并输出:

i=0: 1
i=1: 2 2
i=2: 3 3 3

思路分析

  1. 创建二维数组时,只指定行数(3 行),不指定列数。
  2. 遍历每行,为每行的一维数组分配不同的列数(第 i 行分配 i+1 列)。
  3. 给每行的元素赋值(第 i 行的元素值为 i+1)。

代码实现

java
public class TwoDimensionalArray03 {
  public static void main(String[] args) {
    // 创建二维数组(3行,列数不确定)
    int[][] arr = new int[3][];

    // 为每行分配列数并赋值
    for (int i = 0; i < arr.length; i++) {
      arr[i] = new int[i + 1]; // 第i行分配i+1列(0行1列,1行2列,2行3列)
      // 给当前行的元素赋值
      for (int j = 0; j < arr[i].length; j++) {
        arr[i][j] = i + 1; // 元素值为行号+1
      }
    }

    // 输出二维数组
    System.out.println("=====arr元素=====");
    for (int i = 0; i < arr.length; i++) {
      for (int j = 0; j < arr[i].length; j++) {
        System.out.print(arr[i][j] + " ");
      }
      System.out.println(); // 换行
    }
  }
}

方式 4:静态初始化

语法

java
类型[][] 数组名 = { {值1,值2,...}, {值1,值2,...}, ... }

示例

java
// 静态初始化二维数组(3行,列数分别为3、3、1)
int[][] arr = { {1,1,1}, {8,8,9}, {100} };

解读

  • 二维数组arr有 3 个元素(每个元素是一维数组)。
  • 第一个一维数组:[1, 1, 1](3 个元素)。
  • 第二个一维数组:[8, 8, 9](3 个元素)。
  • 第三个一维数组:[100](1 个元素)。

案例:二维数组求和

需求

遍历二维数组int arr[][] = { {4,6}, {1,4,5,7}, {-2} },计算所有元素的和。

代码实现

java
public class TwoDimensionalArray05 {
  public static void main(String[] args) {
    int arr[][] = { {4,6}, {1,4,5,7}, {-2} };
    int sum = 0; // 累积和

    // 嵌套循环遍历二维数组
    for (int i = 0; i < arr.length; i++) {
      for (int j = 0; j < arr[i].length; j++) {
        sum += arr[i][j]; // 累加每个元素
      }
    }

    System.out.println("sum=" + sum); // 输出:4+6+1+4+5+7+(-2) = 25
  }
}

案例:打印杨辉三角

杨辉三角规律

  1. 第 1 行有 1 个元素,第 n 行有 n 个元素。
  2. 每行的第一个元素和最后一个元素都是 1。
  3. 从第 3 行开始,中间元素的值 = 上一行同列元素 + 上一行前一列元素(arr[i][j] = arr[i-1][j] + arr[i-1][j-1])。

代码实现:(打印 10 行杨辉三角)

java
public class YangHui {
  public static void main(String[] args) {
    int rows = 10; // 杨辉三角的行数
    int[][] yangHui = new int[rows][]; // 动态初始化二维数组(行数固定,列数不确定)

    // 给杨辉三角赋值
    for (int i = 0; i < yangHui.length; i++) {
      yangHui[i] = new int[i + 1]; // 第i行有i+1个元素
      // 给每行的第一个和最后一个元素赋值1
      yangHui[i][0] = 1;
      yangHui[i][i] = 1;
      // 给中间元素赋值(从第3行开始,i>=2)
      if (i >= 2) {
        for (int j = 1; j < yangHui[i].length - 1; j++) {
          yangHui[i][j] = yangHui[i-1][j] + yangHui[i-1][j-1];
        }
      }
    }

    // 输出杨辉三角
    System.out.println("===杨辉三角(" + rows + "行)===");
    for (int i = 0; i < yangHui.length; i++) {
      for (int j = 0; j < yangHui[i].length; j++) {
        System.out.print(yangHui[i][j] + "\t");
      }
      System.out.println(); // 换行
    }
  }
}

二维数组注意事项

  1. 一维数组的声明方式int[] xint x[]

  2. 二维数组的声明方式int[][] yint[] y[]int y[][](三种都合法,推荐第一种)。

  3. 二维数组由多个一维数组组成,各个一维数组的长度可以不同(列数不等的二维数组)。

    java
    // map[0]有2个元素,map[1]有3个元素
    int map[][] = { {1,2}, {3,4,5} };

练习

声明int[] x, y[];,以下选项允许通过编译的是(BE)

  • a) x[0] = y; → 错误(int 类型不能接收 int[][]类型)
  • b) y[0] = x; → 正确(int[]类型接收 int[]类型)
  • c) y[0][0] = x; → 错误(int 类型不能接收 int[]类型)
  • d) x[0][0] = y; → 错误(x 是一维数组,x[0][0]语法错误)
  • e) y[0][0] = x[0]; → 正确(int 类型接收 int 类型)
  • f) x = y; → 错误(int[]类型不能接收 int[][]类型)

Arrays~

java.util.Arrays 是 Java 标准库中非常重要的一个工具类。该类专门用于操作数组(如排序、查找、复制、填充和转换等)。

因为它的所有方法都是静态的,且构造方法被私有化(private),所以你不需要也不能实例化它,直接通过 Arrays.方法名() 调用即可。

数组内存表现

在 Java 中,数组属于引用数据类型。无论是基本数据类型数组还是对象数组,底层在堆内存(Heap) 中都是一块连续的存储空间

  • 基本数据类型数组:空间内直接存储元素的值
  • 对象数组:空间内存储的是指向各个对象的引用(内存地址)

数组排序算法

Arrays 类在实现排序时,并非盲目使用单一算法,而是根据数据类型和规模进行了极致的工程优化

  • 基本数据类型排序:采用 Dual-Pivot Quicksort(双轴快速排序)。该算法是对传统快排的改进,在绝大多数数据集上能达到 O(nlogn)O(n \log n) 的时间复杂度,且表现出比传统快排更好的扫描效率。

  • 对象类型排序:采用 Timsort(混合稳定排序)。这是一种结合了归并排序和插入排序的算法,专门针对已经部分排序的数据进行优化,最坏时间复杂度为 O(nlogn)O(n \log n),且保证了排序的稳定性(相同元素的相对顺序在排序后保持不变)。

    java
    import java.util.Arrays;
    
    public class ArraysCorePrinciple {
     public static void main(String[] args) {
         // 1. 基本数据类型数组初始化(堆内存中分配连续空间存储数值
         int[] primitiveArray = {3, 1, 4, 1, 5};
    
         // 2. 对象类型数组初始化(堆内存中分配连续空间存储引用地址
         String[] objectArray = {"Java", "Python", "C++"};
    
         // 3. 工具类直接操作引用
         System.out.println("基本数组:" + Arrays.toString(primitiveArray));
         System.out.println("对象数组:" + Arrays.toString(objectArray));
     }
    }

API:Arrays

排序

本功能组涵盖了对数组进行原地升序排列以及利用多线程提升排序效率的 API。

  • void Arrays.sort()(基本类型[] a),对指定的 基本类型数组按数字升序进行排序。底层使用双轴快速排序算法。

  • <T> void Arrays.sort()(T[] a, Comparator<? super T> c),根据指定的比较器(Comparator)对对象数组进行原地定制排序。底层使用 Timsort 算法。

  • void Arrays.parallelSort()(基本类型[] a),利用 ForkJoin 框架对指定的 基本类型数组进行并行排序。在多核 CPU 且大数据量(通常元素量大于 1 << 13)的场景下,效率显著高于单线程排序。

注意事项:

  1. 使用 Arrays.sort(T[] a, Comparator c) 时,比较器必须严格遵守传递性(若 a > b 且 b > c,则 a > c)和自反性。否则在 JDK 1.8 及以上版本中,底层 Timsort 会抛出 IllegalArgumentException: Comparison method violates its general contract! 异常。
  2. 并行排序 parallelSort 会消耗额外的线程资源,如果数组规模较小(如只有几十个元素),其线程上下文切换的开销反而会导致性能不如普通的 sort
java
import java.util.Arrays;
import java.util.Comparator;

public class ArraysSortFunctionalGroup {
  public static void main(String[] args) {
    // 示例 1: 基本数据类型原地排序
    int[] scores = {89, 95, 76, 100, 60};
    Arrays.sort(scores);
    System.out.println("基本类型排序结果: " + Arrays.toString(scores));

    // 示例 2: 对象数组定制排序(降序)
    Integer[] sizes = {10, 50, 30, 20, 40};
    Arrays.sort(sizes, new Comparator<Integer>() {
      @Override
      public int compare(Integer o1, Integer o2) {
        return Integer.compare(o2, o1); // 逆序比较
      }
    });
    System.out.println("对象类型定制排序结果: " + Arrays.toString(sizes));

    // 示例 3: 并行排序(模拟大数据量场景)
    int[] largeArray = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    Arrays.parallelSort(largeArray);
    System.out.println("并行排序结果: " + Arrays.toString(largeArray));
  }
}

二分查找

本功能组用于在已排序的数组中快速定位指定元素的索引。

  • int Arrays.binarySearch()(基本类型[] a, 基本类型 key),使用二分搜索算法在基本类型数组中寻找指定键值的索引。

  • <T> int Arrays.binarySearch()(T[] a, T key, Comparator<? super T> c),使用指定的比较器在已排序的对象数组中通过二分法寻找指定目标的索引。

注意事项:

  1. 前置条件:在调用 binarySearch 之前,目标数组必须已经处于升序排列状态(如果使用的是对象数组,排序时所用的比较器必须与查找时使用的比较器完全一致)。若数组未排序,查找结果将是不可预期的。
  2. 返回值逻辑:若数组中包含该目标元素,则返回其对应的索引 index>=0);若数组中不包含该元素,则返回 -(insertion point) - 1。其中 insertion point(插入点)是指该元素如果插入数组中,应该所在的索引位置。
java
import java.util.Arrays;

public class ArraysSearchFunctionalGroup {
  public static void main(String[] args) {
    int[] sortedNumbers = {12, 24, 35, 47, 58, 69};

    // 示例 1: 查找存在的元素
    int targetExist = 35;
    int indexExist = Arrays.binarySearch(sortedNumbers, targetExist);
    System.out.println("元素 " + targetExist + " 的索引为: " + indexExist); // 输出 2

    // 示例 2: 查找不存在的元素
    int targetNotExist = 40;
    int indexNotExist = Arrays.binarySearch(sortedNumbers, targetNotExist);
    // 40 应该被插入在 35(索引2) 和 47(索引3) 之间,即插入点为 3
    // 返回值为:-3 - 1 = -4
    System.out.println("元素 " + targetNotExist + " 的返回值为: " + indexNotExist); // 输出 -4
  }
}

数组填充与复制

本功能组用于批量初始化数组元素以及实现数组的动态扩容或截取。

  • void Arrays.fill()(any[] a, any val)数组填充。将指定的 any 值分配给指定 any 数组的每个元素。常用于数组的初始化或重置。

  • any[] Arrays.copyOf()(any[] original, int newLength)数组扩容复制。复制指定的数组,截取或用 0 填充(如果是对象数组则填充 null),以使副本具有指定的长度。

  • int[] Arrays.copyOfRange()(int[] original, int from, int to)范围截取复制。将指定数组的指定范围(左闭右开区间 [from, to))复制到一个新数组中。

注意事项:

Arrays.copyOf()Arrays.copyOfRange() 对于对象数组的复制属于浅拷贝(Shallow Copy)。它们只复制了对象的引用地址,而没有克隆对象本身。如果修改了新数组中某个对象的属性,原数组中对应对象的属性也会随之改变。

java
import java.util.Arrays;

public class ArraysCopyFillFunctionalGroup {
  public static void main(String[] args) {
    // 示例 1: 数组填充
    int[] data = new int[5];
    Arrays.fill(data, -1);
    System.out.println("填充后的数组: " + Arrays.toString(data));

    // 示例 2: 数组扩容复制
    int[] original = {1, 2, 3};
    int[] expanded = Arrays.copyOf(original, 5); // 扩容到长度 5,末尾补 0
    System.out.println("扩容后的新数组: " + Arrays.toString(expanded));

    // 示例 3: 范围截取复制(左闭右开)
    int[] source = {10, 20, 30, 40, 50};
    int[] subArray = Arrays.copyOfRange(source, 1, 4); // 截取索引 1, 2, 3
    System.out.println("截取后的子数组: " + Arrays.toString(subArray));
  }
}

集合与流式转换

本功能组用于实现数组向集合框架以及函数式流式 API 的平滑转换。

  • <T> List<T> Arrays.asList()(T... a)数组转列表。返回一个受指定数组支持的固定大小的列表。常用于将数组转化为集合进行只读或元素修改操作。

  • <T> Stream<T> Arrays.stream()(T[] array)数组转流。返回以指定对象数组作为源的顺序 Stream 流。用于开启函数式流处理(如过滤、映射)。

  • IntStream Arrays.stream()(基本类型[] array)数组转流。返回以指定基本类型数组作为源的顺序 IntStream 流,避免了基本数据类型的自动装箱开销。

注意事项:

  1. 基本类型陷阱:严禁直接将基本数据类型数组(如 int[])传给 Arrays.asList()。这会导致泛型推导将整个 int[] 视为一个单一对象,最终返回一个 List<int[]> 而非 List<Integer>。正确的做法是使用 Arrays.stream(array).boxed().toList() 或将数组声明为包装类 Integer[]
  2. 结构性变更异常:对 Arrays.asList() 返回的列表执行 add()remove()clear() 操作,会在运行时抛出 UnsupportedOperationException
java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ArraysTransformFunctionalGroup {
  public static void main(String[] args) {
    // 示例 1: 对象数组转换为 List 及其视图联动特性
    String[] heroes = {"IronMan", "Thor", "Hulk"};
    List<String> heroList = Arrays.asList(heroes);

    // 修改 List 中的元素,原数组也会被改变
    heroList.set(1, "DoctorStrange");
    System.out.println("修改 List 后原数组变为: " + Arrays.toString(heroes));

    try {
      heroList.add("Spiderman"); // 触发异常(改变了数组的结构)
    } catch (UnsupportedOperationException e) {
      System.out.println("捕获异常: Arrays.asList 返回的列表不支持结构性扩展");
    }

    // 示例 2: 基本类型数组正确转换为 List 的方式
    int[] primitives = {1, 2, 3};
    List<Integer> correctList = Arrays.stream(primitives).boxed().collect(Collectors.toList());
    // List<Integer> correctList = Arrays.toList(Integer.valueOf(primitives));
    System.out.println("基本类型正确转换后的 List: " + correctList);

    // 示例 3: Stream API 链式处理
    int[] scores = {55, 82, 93, 41, 76};
    long count = Arrays.stream(scores)
      .filter(score -> score >= 60)
      .count();
    System.out.println("及格人数: " + count); // 3
  }
}

数组比较与字符串表示

本功能组用于处理一维及多维数组的相等性逻辑判定与格式化文本输出。

  • boolean Arrays.equals()(基本类型[] a1, 基本类型[] a2)浅层比较。如果两个指定的基本类型数组彼此相等(长度相同且对应位置元素相等),则返回 true。

  • boolean Arrays.deepEquals()(Object[] a1, Object[] a2)深度比较。如果两个指定的对象数组彼此深度相等则返回 true。适用于任意深度的多维嵌套数组。

  • String Arrays.deepToString()(Object[] a)深度打印。返回指定多维数组“深层内容”的字符串表示形式。

注意事项:

对于多维数组(如 int[][]),如果调用普通的 Arrays.equals(),其底层只会通过 == 比较一维节点的引用地址,无法判断内部元素是否相等。必须强制使用 Arrays.deepEquals()

java
import java.util.Arrays;

public class ArraysCompareStringFunctionalGroup {
  public static void main(String[] args) {
    // 示例 1: 一维数组比较与多维数组的错误比较
    int[][] matrix1 = {{1, 2}, {3, 4}};
    int[][] matrix2 = {{1, 2}, {3, 4}};

    // 错误示范:一维浅度比较
    boolean shallowEquals = Arrays.equals(matrix1, matrix2);
    System.out.println("一维 equals 比较多维数组结果: " + shallowEquals); // 打印 false

    // 示例 2: 多维数组的深度比较与深度打印
    boolean deepEquals = Arrays.deepEquals(matrix1, matrix2);
    System.out.println("深度 deepEquals 比较结果: " + deepEquals); // 打印 true
    System.out.println("多维数组深度打印: " + Arrays.deepToString(matrix1));
  }
}

现代增强特性

本功能组包含 Java 9 及后续版本中引入的字典序比较与不匹配点检索的高性能工具方法。

  • int Arrays.compare()(基本类型[] a, 基本类型[] b)字典序比较。按字典顺序比较两个基本类型数组。若第一个数组在字典序上小于、等于、大于第二个数组,则对应返回负数、零、正数。

  • int Arrays.mismatch()(基本类型[] a, 基本类型[] b)不匹配点检索。查找并返回两个基本类型数组之间第一个不匹配元素的索引。如果两个数组完全相同,则返回 -1。

注意事项:

  1. Arrays.compare 的比较基于字典序。如果两个数组的前缀完全相同,则较短的数组在字典序上“小于”较长的数组。
  2. Arrays.mismatch 具有短路(Short-circuiting)特性,一旦在低位索引发现不匹配元素会立即返回,性能优于通过循环手动比对。
java
import java.util.Arrays;

public class ArraysModernFunctionalGroup {
  public static void main(String[] args) {
    int[] arrayA = {1, 2, 3, 4};
    int[] arrayB = {1, 2, 5, 4};
    int[] arrayC = {1, 2, 3};

    // 示例 1: 查找第一个不匹配的索引位置
    int mismatchIndex = Arrays.mismatch(arrayA, arrayB);
    System.out.println("A 和 B 第一个不同元素的索引为: " + mismatchIndex); // 索引为 2 (3 和 5 不同)

    // 示例 2: 字典序比较
    int compareAB = Arrays.compare(arrayA, arrayB); // A 的第三个元素 3 < B 的 5
    int compareAC = Arrays.compare(arrayA, arrayC); // C 是 A 的前缀,A 较长

    System.out.println("A 对比 B (字典序): " + compareAB); // 输出负数
    System.out.println("A 对比 C (字典序): " + compareAC); // 输出正数
  }
}

实战:学生成绩管理

场景说明:

模拟一个简易的学生成绩管理模块。系统输入一组学生的分数,需要完成以下逻辑:

  1. 过滤异常的初始数据并进行初始化重置。

  2. 对成绩进行快速排序。

  3. 提取出前三名(前序截取)的成绩。

  4. 在已排序的分数中,快速精准查找是否存在某个特定的合格线分数。

完整源码:

java
import java.util.Arrays;

public class StudentScoreManager {
  public static void main(String[] args) {
    // 1. 初始化原始成绩数组(包含未录入的默认数据)
    int[] rawScores = {85, 92, 78, 60, 45, 99, 88, 0, 0};
    System.out.println("1. 原始输入的学生成绩: " + Arrays.toString(rawScores));

    // 2. 截取有效成绩(排除末尾未录入的 0 元素)
    // 假设已知前 7 个为有效成绩
    int[] validScores = Arrays.copyOfRange(rawScores, 0, 7);
    System.out.println("2. 截取出的有效成绩: " + Arrays.toString(validScores));

    // 3. 对有效成绩进行升序排列(内部采用双轴快排)
    Arrays.sort(validScores);
    System.out.println("3. 排序后的有效成绩: " + Arrays.toString(validScores));

    // 4. 获取成绩最高的前三名(由于是升序,前三名在数组末尾)
    // 通过 copyOfRange 截取最后三个元素
    int length = validScores.length;
    int[] topThree = Arrays.copyOfRange(validScores, length - 3, length);
    System.out.println("4. 成绩最高的前三名(升序): " + Arrays.toString(topThree));

    // 5. 使用二分查找判断是否存在恰好 78 分的学生
    int targetScore = 78;
    int searchResult = Arrays.binarySearch(validScores, targetScore);

    if (searchResult >= 0) {
      System.out.println("5. 查询成功:存在 " + targetScore + " 分的学生,其在排序后数组中的索引为: " + searchResult);
    } else {
      System.out.println("5. 查询失败:未找到精确为 " + targetScore + " 分的学生。");
    }

    // 6. 重置管理器:清空所有有效数据,用 -1 填充表示不可用状态
    Arrays.fill(validScores, -1);
    System.out.println("6. 重置后的有效成绩数组: " + Arrays.toString(validScores));
  }
}

本章作业

  1. 下面数组定义正确的有(BD)

    • A. String strs[] = {'a','b','c'}; → 错误(字符串数组不能用 char 字面量初始化)
    • B. String[] strs = {"a","b","c"}; → 正确(静态初始化字符串数组)
    • C. String[] strs = new String{"a" "b" "c"}; → 错误(语法错误:缺少逗号和右括号)
    • D. String strs[] = new String[]{"a", "b","c"}; → 正确
    • E. String[] strs = new String[3]{"a", "b", "c"); → 错误(动态初始化指定长度后不能直接赋值元素)
  2. 写出结果

    java
    String foo = "blue";
    boolean[] bar = new boolean[2];
    if (bar[0]) {
      foo = "green";
    }
    System.out.println(foo); // 输出:blue(boolean数组默认值为false,if条件不成立)
  3. 以下 Java 代码的输出结果为()

    java
    int num = 1;
    while (num < 10) {
      System.out.println(num);
      if (num > 5) {
        break;
      }
      num += 2;
    }
    // 输出:1、3、5、7(num=1→3→5→7,7>5触发break)
  4. 有序数组插入元素:已知升序数组[10,12,45,90],插入元素23后保持升序(结果[10,12,23,45,90])。

    实现步骤

    1. 定义原数组和待插入元素:确定升序原数组和需要插入的元素。
    2. 查找插入位置:遍历原数组,找到第一个大于插入元素的位置(即为插入点);若元素比所有元素都大,则插入到数组末尾。
    3. 创建新数组:长度为原数组长度 + 1,用于存储插入后的结果。
    4. 复制元素并插入:将原数组中插入位置前的元素复制到新数组,放入插入元素,再复制插入位置后的元素。
    5. 验证结果:输出新数组,确认升序性。

    代码实现

    java
    // 1. 定义原升序数组和待插入的元素
    int[] arr = {10, 12, 45, 90};
    int insertNum = 23;
    
    // 2. 查找插入位置
    int insertIndex = -1;
    for (int i = 0; i < arr.length; i++) {
        // 找到第一个大于待插入元素的位置,即为插入点
        if (arr[i] > insertNum) {
            insertIndex = i;
            break;
        }
    }
    // 若元素比所有元素都大,插入到数组末尾
    if(insertIndex == -1) insertIndex = arr.length;
    
    // 3. 创建新数组并完成插入
    int[] newArr = new int[arr.length + 1];
    // i:新数组newArr的索引;j:原数组arr的索引
    for(int i=0, j=0; i < newArr.length; i++) {
        if(i != insertIndex) {
            newArr[i] = arr[j];
            j++;
        } else {
            newArr[i] = insertNum;
        }
    }
    
    // 4. 输出结果验证
    System.out.println("原数组:" + Arrays.toString(arr));
    System.out.println("插入元素 " + insertNum + " 后的新数组:" + Arrays.toString(newArr));
  5. 数组综合操作:随机生成 10 个整数(1-100)保存到数组,实现:

    • 倒序打印。
    • 求平均值。
    • 求最大值及下标。
    • 查找是否包含数字 8。
  6. 数组引用传递练习:写出以下代码的打印结果:

    java
    char[] arr1 = {'a','z','b','c'};
    char[] arr2 = arr1;
    arr1[2] = '韩';
    for (int i = 0; i < arr2.length; i++) {
        System.out.println(arr1[i] + "," + arr2[i]);
    }
    // 输出:
    // a,a
    // z,z
    // 韩,韩
    // c,c
  7. 写出冒泡排序的代码:基于冒泡排序案例,独立实现从小到大排序。